/*
 * Copyright (c) Ulrich Hilger, http://dev.uhilger.de, all rights reserved.
 *
 * Published under the terms and conditions of the Apache License 2.0,
 * see http://www.apache.org/licenses/LICENSE-2.0
 */

package de.uhilger.servicehub.server;

import de.uhilger.servicehub.model.*;
import de.uhilger.lib.filesystem.FileRef;
import de.uhilger.lib.filesystem.LocalFileSystem;
import de.uhilger.servicehub.HttpRequest;
import de.uhilger.servicehub.ServiceHub;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyVetoException;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
//import org.apache.catalina.Container;
//import org.apache.catalina.Context;

/**
 * <p>A server that models and implements a servlet container and a database server.</p>
 *
 * <p>The class uses Tomcat and JavaDB in embedded mode to implement the servers.</p>
 *
 * @author Ulrich Hilger, <a href="http://dev.uhilger.de" target="_blank">http://dev.uhilger.de</a>
 * @author Published under the terms and conditions of
 * the <a href="http://www.apache.org/licenses/LICENSE-2.0" target="_blank">Apache License 2.0</a>
 */
public class AppServer {

  /** the character used to separate elements in URLS (/) */
  public static final String URL_SEPARATOR = "/";
  /** name of dbServer driver to use for connections to the server */
  public static final String DERBY_DRIVER_NAME = "org.apache.derby.jdbc.ClientDriver";// "org.apache.dbServer.jdbc.EmbeddedDriver";
  /** JDBC string to use to connect to the database server */
  //public static final String DERBY_CONNECT_STR = "jdbc:derby://localhost:1527/";
  /** name of database */
  public static final String STORAGE_NAME = "store/udb";

  /** parts of server that a used to signal changes */
  public enum ServerPart {BASE, STATUS, HTTP_PORT, JDBC_PORT, APPS, HOME};

  private ServletContainer servletContainer;
  private DatabaseServer dbServer;
  private UserStore userStore;
  private List listeners = new ArrayList();
  private boolean serverRuns = false;
  private int port;
  private int dbPort;
  private String dbUser;
  private String dbPassword;

  /**
   * Create a new object of class AppServer.
   */
  public AppServer(String serverHomeDir, String serverClassName, int dbPort) {
    super();
    //servletContainer = null;
    dbServer = new NetworkDerbyServer(dbPort, new PrintWriter(System.out));
    try {
      ServletContainerFactory.registerServletContainer(serverHomeDir);
      servletContainer = ServletContainerFactory.createServletContainer(serverClassName);
      servletContainer.setUserStorageName(STORAGE_NAME);
      //servletContainer = new EmbeddedTomcat();
    } catch (ClassNotFoundException ex) {
      Logger.getLogger(AppServer.class.getName()).log(Level.SEVERE, null, ex);
    } catch (InstantiationException ex) {
      Logger.getLogger(AppServer.class.getName()).log(Level.SEVERE, null, ex);
    } catch (IllegalAccessException ex) {
      Logger.getLogger(AppServer.class.getName()).log(Level.SEVERE, null, ex);
    } catch (IOException ex) {
      Logger.getLogger(AppServer.class.getName()).log(Level.SEVERE, null, ex);
    }
    //servletContainer = new EmbeddedTomcat();
  }

  /**
   * Get the user store
   * @return the user store this server maintains for users and roles
   */
  public UserStore getUserStore() {
    return userStore;
  }

  public void setDbUser(String dbUser) {
    this.dbUser = dbUser;
    servletContainer.setDbUser(dbUser);
    // don't set the user store user now, the user name set here is used for the user store when the server starts
    // userStore.setDbUser(dbUser);
  }

  public void setDbPassword(String dbPass) {
    this.dbPassword = dbPass;
    if(isRunning()) {
      userStore.changePassword(dbPass);
    } else {
      servletContainer.setDbPassword(dbPass);
    }
    // don't set the user store password now, the password set here is used for the user store when the server starts
    //userStore.setDbPassword(dbPass);
  }


  /**
   * Deploy a file onto the servlet container
   * @param fileName  name of war file to deploy
   * @throws Exception
   */
  public void deploy(String fileName) throws Exception {
    /*if(localhost.map(contextPath) != null/*contextPathExists(contextPath)*//*) {
      throw new Exception("Context " + contextPath + " already exists.");
    } else {
      File file = new File(fileName);
      String urlStr = ServiceHub.JAR_PREFIX + file.toURI().toURL().toString() + "!/";
      ServiceHub.log("deploy urlStr=" + urlStr, getClass(), Level.FINEST);
      ExpandWar.expand(localhost, new URL(urlStr), contextPath);
    }*/
    File file = new File(fileName);
    String urlStr = ServiceHub.JAR_PREFIX + file.toURI().toURL().toString() + "!/";
    ServiceHub.log("deploy urlStr=" + urlStr, getClass(), Level.FINEST);
    //File file = new File(fileName);
    String fileNameExt = file.getName();
    servletContainer.deployWAR(fileNameExt.substring(0, fileNameExt.indexOf(".")), new URL(urlStr));
    fireServerStatusChanged(ServerPart.APPS);
  }

  /**
   * <p>Undeploy a web application</p>
   *
   * <p>This will unregister the context from the servlet container and
   * delete all content inside the given context path.</p>
   *
   * @param contextPath  context path of application to undeploy
   * @throws Exception
   */
  public void undeploy(String contextPath) throws Exception {
    servletContainer.unregisterWAR(contextPath);
    File file = new File(getBase() + AppServer.URL_SEPARATOR +
      ServletContainer.DEFAULT_WEBAPPS_PATH + contextPath);
    ServiceHub.log("undeploy delete " + file.getAbsolutePath(), getClass(), Level.FINEST);
    FileRef appDir = new FileRef(file.getAbsolutePath(), true);
    FileRef[] files = new FileRef[1];
    files[0] = appDir;
    LocalFileSystem fs = new LocalFileSystem();
    fs.delete(files);
    fireServerStatusChanged(ServerPart.APPS);
  }

  public void startApp(String contextPath) throws Exception {
    servletContainer.registerWAR(contextPath, getBase() + ServletContainer.URL_SEPARATOR +
      ServletContainer.DEFAULT_WEBAPPS_PATH + ServletContainer.URL_SEPARATOR + contextPath);
    fireServerStatusChanged(ServerPart.APPS);
  }

  public void stopApp(String contextPath) throws Exception {
    servletContainer.unregisterWAR(contextPath);
    fireServerStatusChanged(ServerPart.APPS);
  }

  /**
   * Get the port number this server services through.
   * @return  the port number
   */
  public int getPort() {
    return port;
  }

  public int getDbPort() {
    return dbPort;
  }

  /**
   * set the port of this server. This will only work when the server already runs.
   * @param newPort  the port number in the range 0-9999 to service through
   * @throws PropertyVetoException when an invalid port number is passed or the server already runs
   */
  public void setPort(int newPort) throws PropertyVetoException {
    if (!isRunning()) {
      if (newPort > -1 && newPort < 10000) {
        this.port = newPort;
        fireServerStatusChanged(ServerPart.HTTP_PORT);
      } else {
        throw new PropertyVetoException("Invalid port number", new PropertyChangeEvent(this, "port", port, newPort));
      }
    } else {
      throw new PropertyVetoException("Cannot set port number while server runs",
              new PropertyChangeEvent(this, "port", port, newPort));
    }
  }

  /**
   * set the database port of this server. This will only work when the server already runs.
   * @param newPort  the port number in the range 0-9999 to service through
   * @throws PropertyVetoException when an invalid port number is passed or the server already runs
   */
  public void setDbPort(int newPort) throws PropertyVetoException {
    if (!isRunning()) {
      if (newPort > -1 && newPort < 10000) {
        this.dbPort = newPort;
        fireServerStatusChanged(ServerPart.JDBC_PORT);
      } else {
        throw new PropertyVetoException("Invalid port number", new PropertyChangeEvent(this, "port", port, newPort));
      }
    } else {
      throw new PropertyVetoException("Cannot set port number while server runs",
              new PropertyChangeEvent(this, "port", dbPort, newPort));
    }
  }

  /**
   * Get the home directory of the servlet container of this server
   * @return  the servlet container home directory
   */
  public String getHome() {
    return servletContainer.getHome();
  }

  /**
   * Set the home directory of the servlet container of this server
   * @param homePath  the servlet container home directory
   */
  public void setHome(String homePath) {
    servletContainer.setHome(homePath);
    fireServerStatusChanged(ServerPart.HOME);
  }

  /**
   * Get the web applications that currently are deployed to the
   * servlet container of this server
   * @return  the web applications
   */
  public String[] getApps() {
    return servletContainer.getApps();
  }

  /**
   * <p>Set the path to the base directory of the servlet container of this server, i.e.
   * the directory that contains directory webapps.</p>
   *
   * @param path  the path to the base directory
   * @throws PropertyVetoException when the server already runs
   */
  public void setBase(String path) throws PropertyVetoException {
    if(!isRunning()) {
      ServiceHub.log("Server about to set server web path to " + path, getClass(), Level.FINEST);
      System.setProperty("derby.system.home", path);
      servletContainer.setBase(path);
      fireServerStatusChanged(ServerPart.BASE);
    } else {
      throw new PropertyVetoException("Cannot set web path while server runs",
              new PropertyChangeEvent(this, "webpath", servletContainer.getBase(), path));
    }
  }
  
  /**
   * <p>Get the path to the base directory of the servlet container of this server, i.e.
   * the directory that contains directory webapps.</p>
   *
   * @return the base directory
   */
  public String getBase() {
    ServiceHub.log("Server about to return server web path of " + servletContainer.getBase(), getClass(), Level.FINEST);
    return servletContainer.getBase();
  }

  /**
   * <p>Start the server, i.e. the servlet container and database server.</p>
   *
   * <p>This will create the base directory of the servlet container and
   * database server with default content when it is not present already.</p>
   *
   * @throws Exception
   */
  public void start() throws Exception {
    ServiceHub.log("starting server", getClass(), Level.FINEST);
    dbServer.start(dbPort);
    userStore = new DerbyUserStore(getBase(), STORAGE_NAME, dbPort);
    userStore.setDbUser(dbUser);
    userStore.setDbPassword(dbPassword);
    servletContainer.setUserStore(userStore);
    servletContainer.start(getPort());
    /*
     * now that the server runs we can notify the UserService web app of the
     * user database login data that is chosen on the servicehub ui
     */
    ServiceHub.log("server runs", getClass(), Level.FINEST);
    try {
    String result = setUserServiceDbLogin(dbUser, dbPassword);
    ServiceHub.log(result, getClass(), Level.FINEST);
    } catch(Exception ex) {
      // do nothing here, the web app seems to be not deployed
    }
    serverRuns = true;
    fireServerStatusChanged(ServerPart.STATUS);
  }

  /**
   * Let the UserService web app know which login data to use for connections to the user database. 
   * 
   * This will not change user name or password in the database, it will only set the UserService web app 
   * to use a given user name and password.
   * 
   * @param dbUser  user name for connetions to the user database
   * @param dbPass  password for connections to the user database
   * @return  the HTTP response, an XML message that shows success or failure to set the credentials
   * @throws Exception
   */
  private String setUserServiceDbLogin(String dbUser, String dbPass) throws Exception {
    ServiceHub.log("setting UserService db password", getClass(), Level.FINEST);
    InputStream in = HttpRequest.get("http://localhost:" + getPort() +
            "/UserService/Admin/setDbLogin?dbu=" + dbUser + "&dbp=" + dbPass);
    BufferedReader br = new BufferedReader(new InputStreamReader(in));
    StringBuffer buf = new StringBuffer();
    String line = br.readLine();
    while(line != null) {
      buf.append(line);
      line = br.readLine();
    }
    in.close();
    br.close();
    return buf.toString();
  }


  /**
   * Get information about the servlet container of this server
   *
   * @return information about the servlet container
   */
  public String getServletContainerInfo() {
    return servletContainer.getInfo();
  }

  /**
   * Get information about the database server
   * @return   database server information
   */
  public String getDatabaseServerInfo() {
    return dbServer.getDatabaseServerInfo();
  }

  /**
   * Stop this server, i.e. shut down servlet container and database server
   * @throws Exception
   */
  public void stop() throws Exception {
    userStore.close();
    dbServer.shutdown();
    servletContainer.stop();
    serverRuns = false;
    fireServerStatusChanged(ServerPart.STATUS);
  }

  /**
   * Determine whether or not this server runs
   * @return  true when this server runs, false if not
   */
  public boolean isRunning() {
    return serverRuns;
  }

  /**
   * Add a listener that listens for status changes
   * @param listener  the listener to be added
   */
  public void addServerStatusListener(ServerStatusListener listener) {
    listeners.add(listener);
  }

  /**
   * Remove a listener that listens for status changes
   * @param listener  the listener to be removed
   */
  public void removeServerStatusListener(ServerStatusListener listener) {
    listeners.remove(listener);
  }

  /**
   * Notify listeners about a status change
   * @param part  the part that changed
   */
  private void fireServerStatusChanged(ServerPart part) {
    ServiceHub.log("Server status changed, part: " + part, getClass(), Level.FINEST);
    Iterator i = listeners.iterator();
    while(i.hasNext()) {
      Object o = i.next();
      if(o != null && o instanceof ServerStatusListener) {
        ((ServerStatusListener) o).serverStatusChanged(this, part);
      }
    }
  }

}
